For-loops

For loops; what are they? A super useful construct that allows us to do hundreds of calculations with 2-3 lines of code. The Syntax:

for {item} in {iterable}:
    {code block}

So what is an iterable? Well basically it is a data-type that can be considered as 'sequence' of items. In this course we have seen three iterables already: strings, lists and the range function. Lets check that out now:


In [1]:
a_string = "12345"
a_list = list(range(1,6))
a_range_object = range(1, 6)

for num in a_range_object:
    print(num, num*num) # prints num and num**2.
    
for num in a_list:
    print(num, "is {}even".format("not " if num % 2 != 0 else "")) # returns num and whether it is/isnot even

for num in a_string:
    print(int(num)) # returns num, after converting it to an int.


1 1
2 4
3 9
4 16
5 25
1 is not even
2 is even
3 is not even
4 is even
5 is not even
1
2
3
4
5

From this code snippet I want you to understand a few things. The first is that "num" takes on all the values 1...5 sequentially. The other (main) thing I want you to realise is that within the context of the loop "num" works just like any other normal variable name. And that means we can test it (is it prime, even, divisible by 6, etc) or operate on it (e.g. +,-,/, etc), append it to lists and so on.

Okay, lets heat things up a little. Lets imagine we have a task which states:

  • find every single possible sequence of length 2 (XY) where X and Y are in the English alphabet.
  • for example: "aa", "ab", "ac"... "zz"

Okay, how many combinations are there? Well, we have 26 possible letters and for each letter there are 26 continuations. 26*26= 676. Thats a lot of work for a human being to do by hand, but fortunately for us we have an unthinking machine that can do it all the grunt work. How could we go about solving this task?


In [1]:
import string
alphabet = string.ascii_lowercase # this string is simply abc...xyz

combinations = [] 
for letter in alphabet:
    for letter2 in alphabet: # a for loop-inside a for-loop! Sexy. 
        sequence = letter + letter2  # aa, ab, ac, etc
        combinations.append(sequence)
        
print(len(combinations)) # length of the list
print(combinations[:10], "...", combinations[-9:]) # because the list is so long we are printing two smaller slices of it.


676
['aa', 'ab', 'ac', 'ad', 'ae', 'af', 'ag', 'ah', 'ai', 'aj'] ... ['zr', 'zs', 'zt', 'zu', 'zv', 'zw', 'zx', 'zy', 'zz']

So as you guys can see, it wasn't actually that difficult to enumerate all the combinations. Using for-loops inside for loops is a powerful way of expressing this type of problem but one should always be aware of the maths involved if we wanted to write code in this way for sequence of length five thats 26**5 or 11881376 possible combinations. And so, although double for-loops is a super useful tool to have at ones disposal the reality is an algorithm of the order O(n**2) (see wiki's 'Big O notation' article) isn't practical for large problem sets.

Cautionary tale, beware of the infinite!

When using for and while loops it is possible to accidentally create programs that never terminate. And thats err, usually bad.

There are several ways to make this mistake and below I'm going to showcase one particular issue. Although, since I don't want to crash my computer I have added a 'safety valve' which will allow us to run the code safely.


In [2]:
lst = [1,2]

loop_counter = 0
for item in lst:
    lst.append(item+2)
    loop_counter +=1         # every time we go through the loop, we add one to our counter. 
    if exit_counter == 100:  # if we have gone through the loop 100 times...
        break                # we escape the loop.
print(lst)


[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102]

Okay so this code starts with a list of 2 items, and for each item it adds a new item to the list (item + 2). Because we are constantly adding one to the list we never reach the end of the list, and so therefore the program never terminates.

The 'bandage' fix in place here is a loop_counter. This simply keeps check of how many times we have gone through the loop. Once we hit 100 loops we execute "break" to escape the loop. And thus the program doesn't waltz toward infinity.

There is another (often better) way to handle this issue however, and that is to separate out the thing we wish to iterate over and the thing we wish to change. Here's an example:


In [3]:
lst = [1,2]

for item in lst[:]:  # <--- see lecture on splicing, lst[:] is a COPY of the lst, not the list itself.
    lst.append(item+2)
print(lst)


[1, 2, 3, 4]

The difference here, as noted in the comment, is that lst[:] is a copy of lst and this copy doesn't actually change as lst changes, and so therefore the program terminates.

Homework Assignment

Your task this week is to make a program named "fizzbuzz". The rules

  • Your program should go through the numbers 1 to 100,
  • if the number is even, print "fizz"
  • if the number is divisible by 5 print "buzz"
  • if the number is both even and divisible by 5 print "fizzbuzz"
  • otherwise just print the number.

For extra points:

For extra points you need to make your code easily modifiable. I want to be able to:

  • change the string printed out (e.g change 'fizz' to 'dave', or some other string)
  • be able to change the total
  • change one(or both) of the rules (e.g. change the rule to be divisible by 6, not 5)

To do this you should ask the user what the rules should be (Hint you should use Pythons "input" Function).


In [ ]:
# YOUR CODE HERE